package org.snlab.ddmsh;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.uuid.Generators;

import org.jgrapht.Graphs;
import org.snlab.proto.Ddmpt;
import org.snlab.proto.RuntimeGrpc;
import org.snlab.proto.Ddmpt.Empty;
import org.snlab.proto.Ddmpt.VerifyTask;
import org.snlab.proto.Ddmpt.Vnode;
import org.snlab.proto.Ddmpt.Vnodes;
import org.snlab.proto.RuntimeGrpc.RuntimeBlockingStub;
import org.snlab.proto.RuntimeGrpc.RuntimeStub;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;

import io.grpc.Channel;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

@ShellComponent
public class MyCmd {
    private String envDir;
    private Map<String, Vnodes.Builder> deviceToVnodes = new HashMap<>();

    private List<DFA> buildDFAs() {
        List<DFA> dfas = new ArrayList<>();
        File dfaFiles[] = new File(envDir + "/gen/noptdfas").listFiles();
        for (File file : dfaFiles) {
            DFA dfa = DFA.fromFileOpt(file);
            System.out.println(file.getAbsolutePath());
            dfas.add(dfa);
        }
        return dfas;
    }

    @ShellMethod("deploy DFAs")
    public void deploy(String envDir) {
        this.envDir = envDir;
        assignAddr();
        List<DFA> dfas = buildDFAs();
        for (DFA dfa : dfas) {
            
            Ddmsh.nameToDFA.put(dfa.getName(), dfa);
            for (DFANode dfaNode : dfa.getGraph().vertexSet()) {
                Vnode.Builder vnodeBuilder = Vnode.newBuilder();
                List<DFANode> downs = Graphs.successorListOf(dfa.getGraph(), dfaNode);
                List<DFANode> ups = Graphs.predecessorListOf(dfa.getGraph(), dfaNode);
                List<Ddmpt.To> tos = new ArrayList<>();
                List<Ddmpt.From> froms = new ArrayList<>();
                for (DFANode down : downs) {
                    System.out.println(down.getDevice());
                    Ddmpt.To to = Ddmpt.To.newBuilder().setVid(down.getUuid())
                            .setAddr(Ddmsh.deviceToAddr.get(down.getDevice()))
                            .setEport(dfa.getGraph().getEdge(dfaNode, down).getSymbol()).build();
                    tos.add(to);
                }

                for (DFANode up : ups) {
                    Ddmpt.From from = Ddmpt.From.newBuilder().setVid(up.getUuid()).build();
                    froms.add(from);
                }
                vnodeBuilder.setDfa(dfaNode.getDFA().getName()).setVid(dfaNode.getUuid()).addAllTo(tos)
                        .addAllFrom(froms);
                Vnode vnode = vnodeBuilder.build();
                deviceToVnodes.putIfAbsent(dfaNode.getDevice(), Vnodes.newBuilder());
                deviceToVnodes.get(dfaNode.getDevice()).addVnode(vnode);
            }
        }

        for (Map.Entry<String, Vnodes.Builder> entry : deviceToVnodes.entrySet()) {
            Ddmsh.deviceToStub.get(entry.getKey()).addVnodes(entry.getValue().build());
        }
    }

    @ShellMethod("deploy opt_dfa.txt")
    public void deployOpt(String envDir) {
        this.envDir = envDir;
        assignAddr();
        DFA dfa = DFA.fromFileOpt(new File(envDir + "/gen/dfas/opt_dfa.txt"));
        Ddmsh.nameToDFA.put("opt_dfa.txt", dfa);
        for (DFANode dfaNode : dfa.getGraph().vertexSet()) {
            Vnode.Builder vnodeBuilder = Vnode.newBuilder();
            List<DFANode> downs = Graphs.successorListOf(dfa.getGraph(), dfaNode);
            List<DFANode> ups = Graphs.predecessorListOf(dfa.getGraph(), dfaNode);
            List<Ddmpt.To> tos = new ArrayList<>();
            List<Ddmpt.From> froms = new ArrayList<>();
            for (DFANode down : downs) {
                Ddmpt.To to = Ddmpt.To.newBuilder().setVid(down.getUuid())
                        .setAddr(Ddmsh.deviceToAddr.get(down.getDevice()))
                        .setEport(dfa.getGraph().getEdge(dfaNode, down).getSymbol()).build();
                tos.add(to);
            }

            for (DFANode up : ups) {
                Ddmpt.From from = Ddmpt.From.newBuilder().setVid(up.getUuid()).build();
                froms.add(from);
            }
            vnodeBuilder.setDfa(dfaNode.getDFA().getName()).setVid(dfaNode.getUuid()).addAllTo(tos).addAllFrom(froms);
            Vnode vnode = vnodeBuilder.build();
            deviceToVnodes.putIfAbsent(dfaNode.getDevice(), Vnodes.newBuilder());
            deviceToVnodes.get(dfaNode.getDevice()).addVnode(vnode);
        }
        for (Map.Entry<String, Vnodes.Builder> entry : deviceToVnodes.entrySet()) {
            Ddmsh.deviceToStub.get(entry.getKey()).addVnodes(entry.getValue().build());
        }
    }

    @ShellMethod("show vnodes")
    public void showVnodes(String devName) {
        String addr = Ddmsh.deviceToAddr.get(devName);
        ManagedChannel c = ManagedChannelBuilder.forTarget(addr)
                .usePlaintext()
                .build();
        RuntimeBlockingStub stub = RuntimeGrpc.newBlockingStub(c);
        Vnodes vnodes = stub.showVnodes(Empty.newBuilder().build());
        System.out.println(vnodes);
        c.shutdown();
    }

    @ShellMethod("test")
    public void test(String envDir) {
        DFA dfa = DFA.fromFileOpt(new File(envDir + "/gen/dfas/opt_dfa.txt"));
        if (dfa == null) {
            return;
        }
        Set<DFANode> srcNodes = new HashSet<>();
        for (DFANode node : dfa.getGraph().vertexSet()) {
            if (Graphs.predecessorListOf(dfa.getGraph(), node).size() == 0) {
                srcNodes.add(node);
            }
        }
        for (DFANode dfaNode : srcNodes) {
            System.out.println(dfaNode.getDevice() + String.valueOf(dfaNode.getIndex()));
        }
        System.out.println(srcNodes.size());
    }

    @ShellMethod("verify")
    public void verify(String ip, int prefix, String dfaName) {
        DFA dfa = Ddmsh.nameToDFA.get(dfaName);
        if (dfa == null) {
            return;
        }
        Set<DFANode> srcNodes = new HashSet<>();
        for (DFANode node : dfa.getGraph().vertexSet()) {
            if (Graphs.predecessorListOf(dfa.getGraph(), node).size() == 0) {
                srcNodes.add(node);
            }
        }
        System.out.println(srcNodes.size());
        for (DFANode dfaNode : srcNodes) {
            long numip = 0;
            if (ip.contains(".")) {
                numip = ip2long(ip);
            } else {
                numip = Long.valueOf(ip);
            }
            VerifyTask verifyTask = VerifyTask.newBuilder().setIp(numip).setPrefix(prefix)
                    .setVid(dfaNode.getUuid()).build();
            // String addr =
            // Ddmsh.deviceToAddr.get(Ddmsh.vidToDFANode.get(dfaNode.getUuid()).getDevice());
            // ManagedChannel c = ManagedChannelBuilder.forTarget(addr)
            // .usePlaintext()
            // .build();
            // RuntimeBlockingStub stub = RuntimeGrpc.newBlockingStub(c);
            Ddmsh.deviceToStub.get(dfaNode.getDevice()).verify(verifyTask);
            // c.shutdown();
        }

    }

    @ShellMethod("verifyall")
    public void verifyall() throws FileNotFoundException {
        String fn = this.envDir + "/gen/requirements.txt";
        Scanner in = new Scanner(new File(fn));
        while (in.hasNextLine()) {
            String line = in.nextLine();
            String[] tokens = line.split(" ");
            verify(tokens[0], Integer.valueOf(tokens[1]), tokens[2]);
        }
    }

    @ShellMethod("verifyallopt")
    public void verifyallOpt(int k) throws FileNotFoundException {
        // for (int iPod = 0; iPod < k; iPod++) {
        // for (int iRsw = 0; iRsw < 48; iRsw++) {
        verify("0.0.0.0", 0, "opt_dfa.txt");
        // }
        // }
    }

    @ShellMethod("verifyallopt")
    public void verifyallnopt() throws FileNotFoundException {
        File dfaFiles[] = new File(envDir + "/gen/noptdfas").listFiles();
        for (File file : dfaFiles) {
            verify("0.0.0.0", 0, file.getName());
        }
    }

    @ShellMethod("delete rule test with n times")
    public void delete(String device, int n) {
        for (int i = 0; i < n; i++) {
            Ddmsh.deviceToStub.get(device).deleteRule(Empty.newBuilder().build());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    }

    @ShellMethod("dump")
    public void dump(String device) {
        Ddmsh.deviceToStub.get(device).dump(Empty.newBuilder().build());
    }

    @ShellMethod("dumpall")
    public void dumpall() {
        for (RuntimeBlockingStub stub : Ddmsh.deviceToStub.values()) {
            stub.dump(Empty.newBuilder().build());
        }
    }

    @ShellMethod("connect")
    public void connect(String device, String addr) {
        ManagedChannel channel = ManagedChannelBuilder.forTarget(addr)
                .usePlaintext()
                .build();
        RuntimeBlockingStub stub = RuntimeGrpc.newBlockingStub(channel);
        Ddmsh.deviceToStub.put(device, stub);
    }

    @ShellMethod("connect all")
    public void connectAll(String envDir) {
        this.envDir = envDir;
        assignAddr();
    }

    @ShellMethod("replay msgs")
    public void replay(String device) {
        Ddmsh.deviceToStub.get(device).replay(Empty.newBuilder().build());
    }

    public static long ip2long(String ip) {
        String[] items = ip.split("\\.");
        return Long.valueOf(items[0]) << 24 |
                Long.valueOf(items[1]) << 16 |
                Long.valueOf(items[2]) << 8 |
                Long.valueOf(items[3]);
    }

    private void assignAddr() {
        String fn = this.envDir + "/gen/ddmdconfs.yaml";
        DdmdConfs ddmdConfs = new DdmdConfs();
        ObjectMapper objectMapper = new ObjectMapper(new YAMLFactory());
        try {
            ddmdConfs = objectMapper.readValue(new File(fn), DdmdConfs.class);
            for (DdmdConf ddmdConf : ddmdConfs.getDdmdConfs()) {
                Ddmsh.deviceToAddr.put(ddmdConf.getName(), "0.0.0.0:" + ddmdConf.getPort());
                ManagedChannel channel = ManagedChannelBuilder.forTarget(Ddmsh.deviceToAddr.get(ddmdConf.getName()))
                        .usePlaintext()
                        .build();
                RuntimeBlockingStub stub = RuntimeGrpc.newBlockingStub(channel);
                Ddmsh.deviceToStub.put(ddmdConf.getName(), stub);
            }
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private void deployDFA(File dfaFile) {
        DFA dfa = DFA.fromFile(dfaFile);
        Ddmsh.nameToDFA.put(dfaFile.getName(), dfa);
        System.out.println(dfa.getGraph().vertexSet().size());
        System.out.println(dfa.getGraph().edgeSet().size());
        for (DFANode dfaNode : dfa.getGraph().vertexSet()) {
            Vnode.Builder vnodeBuilder = Vnode.newBuilder();
            List<DFANode> downs = Graphs.successorListOf(dfa.getGraph(), dfaNode);
            List<DFANode> ups = Graphs.predecessorListOf(dfa.getGraph(), dfaNode);
            List<Ddmpt.To> tos = new ArrayList<>();
            List<Ddmpt.From> froms = new ArrayList<>();
            for (DFANode down : downs) {
                Ddmpt.To to = Ddmpt.To.newBuilder().setVid(down.getUuid())
                        .setAddr(Ddmsh.deviceToAddr.get(down.getDevice())).build();
                tos.add(to);
            }

            for (DFANode up : ups) {
                Ddmpt.From from = Ddmpt.From.newBuilder().setVid(up.getUuid()).build();
                froms.add(from);
            }
            vnodeBuilder.setDfa(dfaNode.getDFA().getName()).setVid(dfaNode.getUuid()).addAllTo(tos).addAllFrom(froms);
            Vnode vnode = vnodeBuilder.build();
            // String[] addrArr = Ddmsh.deviceToAddr.get(dfaNode.getDevice()).split(":");
            // ManagedChannel c =
            // ManagedChannelBuilder.forTarget(Ddmsh.deviceToAddr.get(dfaNode.getDevice()))
            // .usePlaintext()
            // .build();
            // RuntimeBlockingStub stub = RuntimeGrpc.newBlockingStub(c);
            // System.out.println("adding " + vnode);
            // stub.addVnode(vnode);
            // c.shutdown();
            // System.out.println(dfaNode.getDevice());
            Ddmsh.deviceToStub.get(dfaNode.getDevice()).addVnode(vnode);
            // c.shutdown();
            // RuntimeBlockingStub stub =
            // RuntimeGrpc.newBlockingStub(Ddmsh.deviceToChannel.get(dfaNode.getDevice()));
            // stub.addVnode(vnode);
        }
    }
}
